/**
 * 
 */
package info.niwota.webapp.web;

import info.niwota.commons.Debug;
import info.niwota.commons.acl.AccessControl;
import info.niwota.commons.acl.AccessDeniedException;
import info.niwota.commons.io.Files;
import info.niwota.commons.web.AjaxHanlder;
import info.niwota.commons.web.PageCursor;
import info.niwota.commons.web.PageCursor.Type;
import info.niwota.webapp.acl.CryptoHelper;
import info.niwota.webapp.acl.PermStore;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources.NotFoundException;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import android.util.Log;

/**
 * @author qiangli
 * 
 */
abstract public class PageContentProvider extends BasePageContentProvider {

	public static final String MISSING_IMAGE_ASSET = "missingImage.png";

	public static final String MISSING_IMAGE_RESOURCE = "/info/niwota/webapp/image/missingImage.png";

	private static final String TAG = "info.niwota.webapp.web.PageContentProvider";

	protected AjaxHanlder ajaxHandler = new AjaxHanlder();

	/**
	 * Verify authorization of the label. Label is a grouping of resources
	 * denoted by uri's such as folder for file system, frames (E*Frame app),
	 * group for contacts...
	 * 
	 * @param group
	 * @param user
	 * @param label
	 * @return
	 */
	protected boolean canAccess(int group, String user, String label) {
		String[] pa = getPerm(label);
		String perm = pa[0];
		String emails = pa[1];
		return AccessControl.canAccess(group, user, label, perm, emails);
	}

	/**
	 * Check user against the authorized list.
	 * 
	 * @param user
	 * @param emails
	 * @return
	 */
	protected boolean canShare(String user, String emails) {
		return AccessControl.canShare(user, emails);
	}

	/**
	 * 
	 * @param uri
	 * @param group
	 * @param user
	 * @param label
	 * @throws SecurityException
	 */
	protected void checkAccess(Uri uri, int group, String user, String label)
			throws SecurityException {
		if (!canAccess(group, user, label)) {
			throw new AccessDeniedException("Not authorized: " + uri.getPath());
		}
	}

	/**
	 * Verify hash
	 * 
	 * @param uri
	 * @param group
	 * @param user
	 * @throws Exception
	 */
	protected void checkHash(Uri uri, int group, String user) throws Exception {
		String hash = uri.getQueryParameter("hash");
		String label = uri.getQueryParameter("label");
		String nhash = CryptoHelper.hash(uri.getPath(), label, group, user,
				false);
		if (Debug.DEBUG) {
			Log.d(TAG, "hash: " + hash + " computed hash: " + nhash + " uri: "
					+ uri);
		}
		if (!nhash.equals(hash)) {
			throw new AccessDeniedException("Invalid hash/uri " + uri);
		}
	}

	protected ParcelFileDescriptor createParcelFileDescriptorFromRes(String name)
			throws IOException {
		int resid = mapResource(name);
		InputStream is = getContext().getResources().openRawResource(resid);
		return createParcelFileDescriptor(is);
	}

	/**
	 * Dispatch based on path.
	 * 
	 * If you follow the following naming conventions, resources under /assets,
	 * /res can be accessed as is, otherwise, you need to override the
	 * appropriate methods to handle your special cases.
	 * 
	 * http://<ip>:10080/content/<pkg>/assets/
	 * http://<ip>:10080/content/<pkg>/res/
	 * 
	 * where ip is the Internet address the phone is serving the requests on and
	 * pkg is the package name of your application.
	 * 
	 * assets/image/ or .png .gif .jpg .tif .ico for images assets/script/ or
	 * .js for javascripts assets/style/ or .css for stylesheets assets/web/ or
	 * *.html HTML files
	 * 
	 * res/ for any resources
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @throws IOException
	 * @return true if handled
	 */
	protected boolean dispatch(PageCursor c, Uri uri, int group, String user,
			String path) throws Exception {
		if (Debug.DEBUG) {
			Log.d(TAG, "dispatch uri: " + uri + " group: " + group + " user: "
					+ user);
		}

		// home requested
		if (path.equals("/") || path.equals("/home") || TextUtils.isEmpty(path)) {
			showMainPage(c, uri, group, user, path);
			return true;
		}

		// handle assets
		final int asprefix = 8;// length of /assets/;
		final boolean isAsset = path.startsWith("/assets/");
		// custom processing
		if (isAsset && showAsset(c, uri, group, user, path.substring(asprefix))) {
			return true;
		}
		// conventional file types: image, script, style
		if (isAsset && isImage(path)) {
			return handleAsset(c, path.substring(asprefix), IMAGE_ANY);
		}
		if (isAsset && isScript(path)) {
			return handleAsset(c, path.substring(asprefix), TEXT_JAVASCRIPT);
		}
		if (isAsset && isStyle(path)) {
			return handleAsset(c, path.substring(asprefix), TEXT_CSS);
		}
		//
		if (isAsset) {
			return handleAsset(c, path.substring(asprefix));
		}

		// handle resources in res
		final int resprefix = 5; // length of /res/
		if (path.startsWith("/res/")) {
			// custom processing
			if (showRes(c, uri, group, user, path.substring(resprefix))) {
				return true;
			}
			//
			if (path.startsWith("/res/drawable/")) {
				return showResImage(c, uri, group, user, path
						.substring(resprefix));
			}
		}

		// handle class resource
		final int clprefix = 6; // length of /class, preserving a leading slash
		if (path.startsWith("/class/")) {
			return showClassResource(c, uri, group, user, path
					.substring(clprefix));
		}

		// json
		if (path.startsWith("/jsonp/")) {
			return handleJson(c, uri, group, user, path.substring(6));
		}

		//
		// handle default
		if (dispatchDefault(c, uri, group, user, path)) {
			return true;
		}

		// custom handling
		if (showContent(c, uri, group, user, path)) {
			return true;
		}
		//
		return false;
	}

	/**
	 * assets/ or res/ can be omitted for sub folders image/ script/ style/ web/
	 * or drawable/
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @return true if handled
	 * @throws Exception
	 */
	protected boolean dispatchDefault(PageCursor c, Uri uri, int group,
			String user, String path) throws Exception {
		if (Debug.DEBUG) {
			Log.d(TAG, "dispatchDefault uri: " + uri + " group: " + group
					+ " user: " + user + " path: " + path);
		}
		//
		// handle assets
		final int asPrefix = 1;
		if (path.startsWith("/image/")) {
			return handleAsset(c, path.substring(asPrefix), IMAGE_ANY);
		}
		if (path.startsWith("/script/")) {
			return handleAsset(c, path.substring(asPrefix), TEXT_JAVASCRIPT);
		}
		if (path.startsWith("/style/")) {
			return handleAsset(c, path.substring(asPrefix), TEXT_CSS);
		}
		// own
		if (path.startsWith("/web/")) {
			return handleAsset(c, path.substring(asPrefix));
		}
		// third party
		if (path.startsWith("/opt/")) {
			return handleAsset(c, path.substring(asPrefix));
		}

		// handle res
		if (path.startsWith("/drawable/")) {
			return showResImage(c, uri, group, user, path.substring(1));
		}
		//
		return false;
	}

	/**
	 * Create parent if file does not exist denoted by the pathname
	 * 
	 * @param ctx
	 * @param pathname
	 * @return
	 */
	protected File getCacheFile(String pathname) {
		final Context ctx = getContext();
		return Files.camouflage(ctx, pathname);
	}

	protected String getLabel(Uri uri) {
		return null;
	}

	protected String[] getPerm(String label) {
		final Context ctx = getContext();
		return PermStore.getPerm(ctx, label);
	}

	protected String guessContentType(String name, String deftype) {
		// remove .cmf before guessing
		if (name.endsWith(Files.CAMOUFLAGE)) {
			int idx = name.lastIndexOf(".");
			name = name.substring(0, idx);
		}
		String type = Files.guessContentType(name, deftype);
		if (Debug.DEBUG) {
			Log.d(TAG, " " + type);
		}
		return type;
	}

	/**
	 */
	protected ParcelFileDescriptor handle(Uri uri, String mode)
			throws Exception {
		String scheme = uri.getScheme();
		if (Debug.DEBUG) {
			Log.d(TAG, "*** ParcelFileDescriptor handle file uri: " + uri
					+ " mode: " + mode + " scheme: " + scheme);
		}
		//
		String path = uri.getPath();
		//
		if ("file".equals(scheme)) {
			return ParcelFileDescriptor.open(new File(path),
					ParcelFileDescriptor.MODE_READ_ONLY);
		}
		//
		File file = new File(uri.getPath());
		if (file.exists()) {
			return createParcelFileDescriptor(file);
		}
		// return createParcelFileDescriptor((String) data);
		// return createParcelFileDescriptor((InputStream) data)
		throw new NotFoundException(uri.toString());
	}

	@Override
	protected PageCursor handle(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) throws Exception {
		String path = uri.getPath();
		if (Debug.DEBUG) {
			Log.d(TAG, "PageCursor handle uri: " + uri + " path: " + path
					+ " projection: " + projection + " selection: " + selection
					+ " selectionArgs: " + selectionArgs + " sortOrder: "
					+ sortOrder);
		}

		PageCursor c = new PageCursor();
		String group = uri.getQueryParameter("group");
		String user = uri.getQueryParameter("user");
		int g = (TextUtils.isEmpty(group) ? AccessControl.ACCESS_PUBLIC
				: Integer.parseInt(group));

		if (dispatch(c, uri, g, user, path)) {
			return c;
		}

		throw new NotFoundException("Can't handle: " + uri);
	}

	protected boolean handleAsset(PageCursor c, String pathname)
			throws Exception {
		final String mimetype = guessContentType(pathname,
				APPLICATION_OCTET_STREAM);
		return handleAsset(c, pathname, mimetype);
	}

	protected boolean handleAsset(PageCursor c, String pathname, String mimetype)
			throws Exception {
		c.contentType = mimetype;

		Context ctx = getContext();
		AssetManager am = ctx.getAssets();
		if (mimetype.startsWith("text/")) {
			c.type = PageCursor.Type.CONTENT;
			// text
			InputStream is = am.open(pathname);
			String s = toString(is);
			c.content = s;
			c.contentLength = s.length();
		} else {
			c.type = PageCursor.Type.URI;
			// audio image video
			// application
			//
			// cache asset
			File f = getCacheFile(pathname);
			if (!f.exists()) {
				copyAsset(pathname, f);
			}
			c.contentLength = f.length();
			c.uri = f.toURI().toString();
		}
		//
		return true;
	}

	protected boolean handleAssetData(PageCursor c, String name,
			String mimetype, JSONObject data) throws Exception {
		c.type = PageCursor.Type.CONTENT;
		Context ctx = getContext();
		AssetManager am = ctx.getAssets();
		InputStream is = am.open(name);
		String s = toString(is) + toDataScript(data);
		c.content = s;
		c.contentLength = s.length();
		c.contentType = mimetype;
		//
		return true;
	}

	protected void handleImage(PageCursor c, String u, int group, String user)
			throws Exception {
		Uri uri = Uri.parse(u);
		if (Debug.DEBUG) {
			Log.d(TAG, "handleImage uri: " + uri);
		}

		checkHash(uri, group, user);

		//
		final String scheme = uri.getScheme();
		//
		String path = null;
		if ("file".equals(scheme)) {
			path = uri.getPath();
		} else {
			// media store
			path = Files.getDataPath(getContext(), uri);
		}
		if (Debug.DEBUG) {
			Log.d(TAG, "uri file path: " + path);
		}
		File f = new File(path);
		if (!f.exists()) {
			throw new IOException("Uri path is not a file: " + uri);
		}
		c.contentLength = f.length();
		c.type = PageCursor.Type.URI;
		c.uri = f.toURI().toString();
	}

	/**
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param p
	 * @return
	 * @throws JSONException
	 */
	protected boolean handleJson(PageCursor c, Uri uri, int group, String user,
			String p) throws JSONException {
		List<String> segs = uri.getPathSegments();
		String json = uri.getQueryParameter("param0");
		// String jsonp = segs.get(0);
		String name = segs.get(1);
		String method = segs.get(2);
		if (Debug.DEBUG) {
			Log.d(TAG, "handleJson className: " + name + " method: " + method
					+ " uri: " + uri + " p: " + p);
		}
		JSONObject data = (json == null ? null : (JSONObject) new JSONTokener(
				json).nextValue());
		try {
			Object obj = ajaxHandler.handle(name, method, data, uri);
			
			String cb = uri.getQueryParameter("callback");
			if (TextUtils.isEmpty(cb)) {
				showJson(c, obj.toString());
			} else {
				String s = cb + "(" + obj.toString() + ")";
				showJson(c, s);
			}
		} catch (Exception e) {
			showError(c, e);
		}
		return true;
	}

	protected boolean showJson(PageCursor c, String s) {
		c.type = PageCursor.Type.CONTENT;
		c.content = s;
		c.contentLength = s.length();
		c.contentType = APPLICATION_JSON;
		return true;
	}

	/**
	 * check for extensions: .html .tpl .htm
	 * 
	 * @param p
	 * @return
	 */
	protected boolean isHtml(String p) {
		final String[] ends = new String[] { ".html", ".tpl", ".htm" };
		for (int i = 0; i < ends.length; i++) {
			if (p.endsWith(ends[i])) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Override to add or limit image types.
	 * 
	 * ".png", ".gif", ".jpg", ".ico"
	 * 
	 * @param path
	 * @return true if path ends in ".png", ".gif", ".jpg", ".ico"
	 */
	protected boolean isImage(String path) {
		final String[] ends = { ".png", ".gif", ".jpg", ".ico" };
		for (int i = 0; i < ends.length; i++) {
			if (path.endsWith(ends[i])) {
				return true;
			}
		}
		return false;
	}

	protected boolean isScript(String path) {
		return path.endsWith(".js");
	}

	protected boolean isStyle(String path) {
		return path.endsWith(".css");
	}

	/**
	 * Map path (resource name) to real resid in R.java
	 * 
	 * @param relpath type/name
	 * @return
	 */
	protected int mapResource(String relpath) {
		final Context ctx = getContext();
		String p = ctx.getPackageName() + ":" + relpath;
		return ctx.getResources().getIdentifier(p, null, null);
	}

	/**
	 * Custom handling of assets of those not conforming to the naming
	 * conventions. Default does nothing.
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @return
	 */
	protected boolean showAsset(PageCursor c, Uri uri, int group, String user,
			String path) throws Exception {
		return false;
	}

	/**
	 * Provide custom background image
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 */
	protected boolean showAssetImage(PageCursor c, String path)
			throws Exception {

		return true;
	}

	/**
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @return
	 * @throws IOException
	 */
	protected boolean showClassResource(PageCursor c, Uri uri, int group,
			String user, String path) throws Exception {
		// cache resource
		File f = getCacheFile(path);
		if (!f.exists()) {
			copyClassResource(path, f);
		}
		c.contentLength = f.length();
		c.type = PageCursor.Type.URI;
		c.uri = f.toURI().toString();
		c.contentType = "image/*";
		return true;
	}

	/**
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @return true if handled
	 * @throws Exception
	 */
	protected abstract boolean showContent(PageCursor c, Uri uri, int group,
			String user, String path) throws Exception;

	/**
	 * Show main page (default, index, welcome, home whatever you call it)
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @throws IOException
	 */
	protected void showMainPage(PageCursor c, Uri uri, int group, String user,
			String path) throws Exception {
		handleAsset(c, "index.html", "text/html");
	}

	/**
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param path
	 * @return true if handled
	 * @throws Exception
	 */
	protected boolean showRes(PageCursor c, Uri uri, int group, String user,
			String path) throws Exception {
		return false;
	}

	/**
	 * 
	 * @param c
	 * @param uri
	 * @param group
	 * @param user
	 * @param relpath
	 * @return
	 * @throws Exception
	 */
	protected boolean showResImage(PageCursor c, Uri uri, int group,
			String user, String relpath) throws Exception {
		// cache resource
		File f = getCacheFile(relpath);
		if (!f.exists()) {
			int resId = mapResource(relpath);
			if (resId > 0) {
				copyRes(resId, f);
			} else {
				throw new NotFoundException(relpath);
			}
		}
		c.contentLength = f.length();
		c.type = PageCursor.Type.URI;
		c.uri = f.toURI().toString();
		c.contentType = "image/*";
		return true;
	}

	protected String toDataScript(JSONObject data) throws JSONException {
		StringBuilder sb = new StringBuilder();
		sb.append("\r\n<script type=\"text/javascript\">\r\n");
		sb.append("var data =\r\n");
		sb.append(data.toString(4));
		sb.append(";\r\n");
		sb.append("</script>");
		return sb.toString();
	}

	protected String toString(InputStream is) throws IOException {
		StringBuffer sb = new StringBuffer();
		copy(is, sb);
		is.close();
		return sb.toString();
	}
	
	protected void redirect(PageCursor c, String newUri) {
		c.type = REDIRECT;
		c.uri = newUri;
	}

	protected void showFile(PageCursor c, File f, String type) {
		c.type = Type.URI;
		c.contentType = type;
		c.contentLength = f.length();
		c.uri = f.toURI().toString();
	}

}
